/*
 * File database TAD.
 * $Rev: 166 $
 */

#include <stdio.h>     /* FILE, fopen, fseek, fwrite, fread, fclose, rename */
#include <stdlib.h>    /* calloc */
#include <stdbool.h>   /* bool */
#include <assert.h>    /* assert */
#include <libgen.h>    /* basename */
#include <string.h>    /* strdup, strcat */
#include <sys/stat.h>  /* mdkir */
#include <sys/types.h> /* mdkir */
#include <unistd.h>    /* ftruncate */
#include <dirent.h>    /* opendir, readdir, closedir */
#include <glob.h>      /* glob */

#include "hash_table/hashtable.h"
#include "hash_table/hashtable_itr.h"
#include "md5lib/md5.h"       /* md5_init, md5_append, md5_finish */
#include "md5lib/md5_utils.h" /* slice_md5 */
#include "utils.h"            /* path_is_directory */
#include "file_db.h"

#define PARTIAL_SUFFIX "partial"

struct _file_db {
    char *path;
    char *partial_path;
    struct hashtable *cache;
};

struct _file {
    char *path;
    char *name;
    md5_t md5;
    off_t size;
    bool is_partial;
    md5_t *index_info;
};

/********************************** Statics ***********************************/

static char *find_file(char *a_path, md5_t md5) {
#   define GLOB_WILDCARD ".*"

    char *res = NULL, *pattern = NULL, *glob_exp = NULL;
    glob_t pglob;
    int matches = 0;

/* PRE */assert(valid_path_name(a_path));
/* PRE */assert(path_is_directory(a_path));
/* PRE */assert(md5 != NULL);

    glob_exp = calloc(strlen(md5) + strlen(GLOB_WILDCARD) + 1, sizeof(char));
    glob_exp = strcat(glob_exp, md5);
    glob_exp = strcat(glob_exp, GLOB_WILDCARD);
    pattern = path_join(a_path, glob_exp);

    debug("file_db.c: find_file: pattern = %s\n", pattern);
    matches = glob(pattern, GLOB_NOSORT, NULL, &pglob);

    debug("file_db.c: find_file: matches = %i\n", matches);
    if (matches == 0) {
        debug("file_db.c: find_file: pglob.gl_pathc = %i, pglob.gl_pathv[0] = %s\n",
              pglob.gl_pathc, pglob.gl_pathv[0]);
        assert(pglob.gl_pathc == 1);
        res = strdup(pglob.gl_pathv[0]);
    }

    globfree(&pglob);
    free(glob_exp);
    free(pattern);

    return res;
}

/* http://www.cse.yorku.ca/~oz/hash.html

sdbm: this algorithm was created for sdbm (a public-domain
reimplementation of ndbm) database library. it was found to do well in
scrambling bits, causing better distribution of the keys and fewer
splits. it also happens to be a good general hashing function with good
distribution. the actual function is hash(i) = hash(i - 1) * 65599 +
str[i]; what is included below is the faster version used in gawk.
[there is even a faster, duff-device version] the magic constant 65599
was picked out of thin air while experimenting with different constants,
and turns out to be a prime. this is one of the algorithms used in
berkeley db (see sleepycat) and elsewhere. */
static unsigned int hash_from_key_fn(void *key) {
    md5_t str = (md5_t)key;
    unsigned int hash = 0;
    int c;

    while ((c = *str++)) {
        hash = c + (hash << 6) + (hash << 16) - hash;
    }
    return hash;
}

static int keys_equal_fn(void *k1,void *k2) {
    md5_t s1 = (md5_t)k1;
    md5_t s2 = (md5_t)k2;

/* PRE */assert(k1 != NULL);
/* PRE */assert(k2 != NULL);

    return(strcmp(s1, s2) == 0);
}
/********************************** File TAD **********************************/

static file *file_create(const char *a_path, bool is_partial) {
#   define NAME_OFFSET 33 /* Index of actual name inside the path */
    file *res = NULL;
    char *tmp = NULL, *path_dup = NULL;

/* PRE */assert(valid_path_name(a_path));
/* PRE */assert(path_is_file(a_path));

    debug("file_db.c: file_create: a_path %s, is_partial %i\n",
          a_path, is_partial);

    res = calloc(1, sizeof(*res));
    if (res != NULL) {
        res->path = strdup(a_path);

        path_dup = strdup(a_path);
        tmp = strdup(basename(path_dup));
        tmp[32] = '\0';
        res->md5 = strdup(tmp);
        tmp += NAME_OFFSET;
        res->name = strdup(tmp);

        res->size = path_size(a_path);
        res->is_partial = is_partial;
        res->index_info = NULL;

        debug("file_db.c: file_create: res->path %s\n"
              "                        res->md5 %s\n"
              "                        res->name %s\n"
              "                        res->size %li, res->is_partial %i\n",
              res->path, res->md5, res->name, res->size, res->is_partial);

        free(path_dup);
        tmp -= NAME_OFFSET; /* Return it back to original pointer */
        free(tmp);
    }

    return res;
}

static void file_destroy(file *a_file) {
/* PRE */assert(a_file != NULL);

    free(a_file->index_info); // XXX: FIXME: check if index_info must be freed
    free(a_file->name);
    free(a_file->md5);
    free(a_file->path);
    free(a_file);
    a_file = NULL;
}

/* Queries */

bool file_is_partial(file *a_file) {
/* PRE */assert(a_file != NULL);

    return(a_file->is_partial);
}

bool file_has_index_info(file *a_file) {
/* PRE */assert(a_file != NULL);

    return(!a_file->is_partial || a_file->index_info != NULL);
}

fragment_pos_t file_fragment_count(file *a_file){
/* PRE */assert(a_file != NULL);

    return((fragment_pos_t)((a_file->size + (FRAGMENT_SIZE - 1)) / FRAGMENT_SIZE));
}

bool file_valid_fragment_position(file *a_file, fragment_pos_t a_position) {
/* PRE */assert(a_file != NULL);

    return(a_position < file_fragment_count(a_file));
}

/* Access */

off_t file_size(file *a_file) {
/* PRE */assert(a_file != NULL);

    return(a_file->size);
}

char *file_name(file *a_file) {
/* PRE */assert(a_file != NULL);

    return(a_file->name);
}

static char *file_path(file *a_file) {
/* PRE */assert(a_file != NULL);

    return(a_file->path);
}

md5_t file_md5(file *a_file) {
/* PRE */assert(a_file != NULL);

    return(a_file->md5);
}

md5_t *file_index_info(file *a_file) {
/* PRE */assert(a_file != NULL);
/* PRE */assert(file_has_index_info(a_file));

    return(a_file->index_info);
}

size_t file_slice(file *a_file, fragment_pos_t a_position, char *fragment) {
    size_t res = -1;
    FILE *f = NULL;
    int fseek_res = -1;

/* PRE */assert(a_file != NULL);
/* PRE */assert(file_valid_fragment_position(a_file, a_position));

    f = fopen(a_file->path, "rb");
    assert(f != NULL);

    //assert((a_position-1) * FRAGMENT_SIZE < a_file->size);
    fseek_res = fseek(f, (a_position-1) * FRAGMENT_SIZE, SEEK_SET);
    assert(fseek_res == 0);

    res = fread(fragment, sizeof(char), FRAGMENT_SIZE, f);

    return res;
}


md5_t *file_fragments_in_disk(file *a_file, unsigned int *res_len) {
    md5_t *res = NULL;
    unsigned int i = 0;
    off_t offset = 0;

/* PRE */assert(a_file != NULL);
/* PRE */assert(res_len != NULL);

    res = calloc(file_fragment_count(a_file), sizeof(md5_t *));
    assert(res != NULL);
    *res_len = 0;

    offset = 0;
/* PRE */assert(valid_path_name(a_file->path));
/* PRE */assert(path_is_file(a_file->path));

    i = MIN(a_file->size - offset, FRAGMENT_SIZE);
    while (offset < a_file->size) {
        assert(*res_len < file_fragment_count(a_file));
        res[*res_len] = slice_md5(a_file->path, offset, i);
        offset += i;
        *res_len += 1;
        i = MIN(a_file->size - offset, FRAGMENT_SIZE);
    }
    assert(*res_len == file_fragment_count(a_file));

    return res;
}

/* Operations */

void file_put_slice(file *a_file, fragment_pos_t a_position, char *data, unsigned int data_len) {
    FILE *f = NULL;
    int f_res = 0;
    md5_state_t state;
    md5_byte_t digest[16];

/* PRE */assert(a_file != NULL);
/* PRE */assert(file_valid_fragment_position(a_file, a_position));
/* PRE */assert(data != NULL);
/* PRE */assert(data_len <= FRAGMENT_SIZE);

    /* Check data */
    md5_init(&state);
    md5_append(&state, (const md5_byte_t *)data, data_len);
    md5_finish(&state, digest);
    assert(!file_has_index_info(a_file) ||
           strcmp(to_hexa(digest, 16), a_file->index_info[a_position]) == 0);

    /* Store! */
    f = fopen (a_file->path, "r+b");
    assert(f != NULL);

    fseek(f, a_position*FRAGMENT_SIZE, SEEK_SET);
    assert(f_res == 0);

    f_res = fwrite(data, data_len, sizeof(*data), f);
    assert((unsigned int)f_res == data_len);

    fclose(f);
    debug("file_db.c: file_put_slice: saving %s:%i", a_file->name, a_position);
}

file *file_set_index_info(file *a_file, md5_t *info, unsigned int info_len) {
/* PRE */assert(a_file != NULL);
/* PRE */assert(info != NULL);
/* PRE */assert(info_len <= file_fragment_count(a_file));

    a_file->index_info = info;
    return a_file;
}

static file *file_set_path(file *a_file, char *a_path) {
/* PRE */assert(a_file != NULL);
/* PRE */assert(a_file->path != NULL);

    free(a_file->path);
    a_file->path = a_path;
    return a_file;
}

static file *file_set_is_partial(file *a_file, bool is_partial) {
/* PRE */assert(a_file != NULL);

    a_file->is_partial = is_partial;
    return a_file;
}

/***************************** File Database TAD *****************************/

file_db *file_db_create(const char *a_path) {
    file_db *res = NULL;
    int mkdir_res = 0;
    struct hashtable *hash_table = NULL;

/* PRE */assert(valid_path_name(a_path));
/* PRE */assert(path_is_directory(a_path));

    res = calloc(1, sizeof(*res));
    if (res != NULL) {
        res->path = strdup(a_path);
        res->partial_path = path_join(a_path, PARTIAL_SUFFIX);
        /* create partial dir if doesn't exist */
        if (!path_is_directory(res->partial_path)) {
            mkdir_res = mkdir(res->partial_path, S_IRWXU); /* This is equivalent to `(S_IRUSR | S_IWUSR | S_IXUSR)'. */
            assert(mkdir_res == 0);
        }
        assert(path_is_directory(res->partial_path));

        /* http://www.cl.cam.ac.uk/~cwc22/hashtable/ */
        hash_table = create_hashtable(16, hash_from_key_fn, keys_equal_fn);
        assert(hash_table != NULL);
        res->cache = hash_table;
    }

    return res;
}

void file_db_destroy(file_db *a_db) {
    struct hashtable_itr *itr = NULL;
    md5_t k = NULL;
    file *v = NULL;

/* PRE */assert(a_db != NULL);

    /* Iterator constructor only returns a valid iterator
     * if the hashtable is not empty */
    if (hashtable_count(a_db->cache) > 0) {
        itr = hashtable_iterator(a_db->cache);
        assert(itr != NULL);
        do {
            k = hashtable_iterator_key(itr);
            v = hashtable_iterator_value(itr);
            assert(v != NULL);
            file_destroy(v);
        } while (hashtable_iterator_advance(itr));
    }
    free(itr);
    hashtable_destroy(a_db->cache, 0);
    free(a_db->partial_path);
    free(a_db->path);
    free(a_db);
    a_db = NULL;
}

/* Access */

file *file_db_get_file(file_db *a_db, md5_t a_file_md5) {
    file *res = NULL;
    int insert_res = 0;
    char *fn = NULL;

/* PRE */assert(a_db != NULL);
/* PRE */assert(a_file_md5 != NULL);

    res = hashtable_search(a_db->cache, a_file_md5);
    debug("file_db.c: file_db_get_file: %s found in cache? %s\n",
          a_file_md5, (res != NULL ? "YES" : "NO"));
    if (res != NULL) { /* Found in cache */
        return res;
    }

    /* Look for complete files */
    fn = find_file(a_db->path, a_file_md5);
    debug("file_db.c: file_db_get_file: %s found in complete files? %s\n",
          a_file_md5, (fn != NULL ? "YES" : "NO"));
    if (fn != NULL) { /* Found in complete files */
        res = file_create(fn, false);
    } else {
        /* Look for partial files */
        fn = find_file(a_db->partial_path, a_file_md5);
        debug("file_db.c: file_db_get_file: %s found in partial files? %s\n",
              a_file_md5, (fn != NULL ? "YES" : "NO"));
        if (fn != NULL) { /* Found in partial files */
            res = file_create(fn, true);
        }
    }

    if (res != NULL) {
        insert_res = hashtable_insert(a_db->cache, strdup(a_file_md5), res);
        if (insert_res == 0) {
            file_destroy(res);
            res = NULL;
        }
    }

    free (fn);
    return res;
}

md5_t *file_db_get_complete_files(file_db *a_db, unsigned int *res_len) {
    md5_t *res = NULL;
    DIR *dir = NULL;
    struct dirent *dir_data = NULL;
    size_t allocated = 1;

/* PRE */assert(a_db != NULL);
/* PRE */assert(res_len != NULL);

    res = calloc(allocated, sizeof(md5_t));
    assert(res != NULL);
    *res_len = 0;

    dir = opendir(a_db->path);
    dir_data = readdir(dir);
    while (dir_data != NULL) {
        char *filename = strdup(dir_data->d_name);

        /* Reallocate res if needed... */
        if (*res_len==allocated) {
            allocated *= 2;
            res = realloc(res, allocated*sizeof(md5_t));
            assert(res != NULL);
        }

        if (filename[0] != '.' && strlen(filename) > 32) {
            filename[32] = '\0'; // Truncate filename leaving only md5
            res[*res_len] = filename;
            debug("file_db.c: file_db_get_complete_files: res[%i] = %s\n", *res_len, res[*res_len]);
            *res_len += 1;
        } else {
            free (filename);
        }
        dir_data = readdir(dir);
    }
    closedir(dir);

    /* We are using a little extra memory, release */
    assert(res != NULL);
    if (*res_len > 0) res = realloc(res, (*res_len)*sizeof(md5_t));
    assert(res != NULL);

    return res;
}

/* Operations */

file *file_db_create_file(file_db *a_db, md5_t a_file_md5,
                  char *a_file_name, size_t a_file_size) {
    file *res = NULL;
    char *tmp = NULL, *fname = NULL;
    FILE *f = NULL;
    int f_res = 0;

/* PRE */assert(a_db != NULL);
/* PRE */assert(a_file_md5 != NULL);
/* PRE */assert(a_file_name != NULL);

    /* 1 extra for "." and 1 extra for terminating \0 */
    tmp = calloc(strlen(a_file_md5) + 1 + strlen(a_file_name) + 1,
                 sizeof(char));
    assert(tmp != NULL);
    tmp = strcat(tmp, a_file_md5);
    tmp = strcat(tmp, ".");
    tmp = strcat(tmp, a_file_name);

    fname = path_join(a_db->partial_path, tmp);
    f = fopen (fname, "ab");
    assert(f != NULL);

    f_res = ftruncate(fileno(f), a_file_size);
    assert(f_res == 0);

    fclose(f);
    free(fname);
    free(tmp);

    res = file_db_get_file(a_db, a_file_md5);
    assert(res != NULL);
    return res;
}

bool file_db_mark_as_complete(file_db *a_db, md5_t a_file_md5) {
    bool res = false;
    file *f = NULL;
    char *tmp = NULL, *md5_in_disk = NULL;
    int rename_res = 0;

/* PRE */assert(a_db != NULL);
/* PRE */assert(a_file_md5 != NULL);
    f = file_db_get_file(a_db, a_file_md5);
/* PRE */assert(f != NULL);
/* PRE */assert(file_is_partial(f));

    /* 1 extra for "/" and 1 extra for "." and 1 extra for terminating \0 */
    tmp = calloc(strlen(a_db->path) + 1 + strlen(file_md5(f)) + 1 + strlen(file_name(f)) + 1,
                 sizeof(char));
    assert(tmp != NULL);
    tmp = strcat(tmp, a_db->path);
    tmp = strcat(tmp, "/");
    tmp = strcat(tmp, file_md5(f));
    tmp = strcat(tmp, ".");
    tmp = strcat(tmp, file_name(f));

    /* check md5 */
    debug("file_db.c: file_db_mark_as_complete: %s\n", file_path(f));
    md5_in_disk = slice_md5(file_path(f), 0, file_size(f));
    assert(strlen(md5_in_disk) == strlen(file_md5(f)));
    res = (strcmp(md5_in_disk,file_md5(f)) == 0);

    debug("file_db.c: file_db_mark_as_complete: %s comparison between:\n"
          "                                     MD5 in disk %s with\n"
          "                                     MD5 in file %s\n",
          (res ? "successful" : "failed"), md5_in_disk, file_md5(f));
    if (res) {
        debug("file_db.c: file_db_mark_as_complete: about renaming %s\n"
              "                                                 to %s\n", file_path(f), tmp);
        rename_res = rename(file_path(f), tmp);
        res = (rename_res == 0);
    }

    file_set_is_partial(f, false);
    file_set_path(f, tmp); /* we need not to free(tmp) now... */

    free(md5_in_disk);

    return res;
}
